//@ts-nocheck
import {CLASS_COMPONENT, ELEMENT, FUNCTION_COMPONENT, TEXT, INSERT, REMOVE, MOVE} from './constants';
import {onlyOne, setProps, flatten, patchProps} from './util';
let updateDepth = 0;
const diffQueue = [];
/** ↓由类组件component forceUpdate调用*/
export function compareTwoElements(oldRenderElement, newRenderElement) {
  oldRenderElement = onlyOne(oldRenderElement);
  newRenderElement = onlyOne(newRenderElement);

  const currentDOM = oldRenderElement/** ←可能是null,因为可能是条件组件*/ && oldRenderElement.dom;

  let currentElement = oldRenderElement;

  //1. 新的没了, 直接移除老节点
  if (newRenderElement == null) { // return ({this.state.number==0?<div></div>:null})
    currentDOM.parentNode.removeChild(currentDOM);
    currentElement = null;

  //2. 节点类型不一致 直接用新的替换旧的
  } else if (oldRenderElement.type != newRenderElement.type) { //span(字符串) div(字符串)... function(函数本身) class(类本身)   文本节点的话 比较特殊 它的type = TEXT = Symbol.for('TEXT')
    const newDom = createDOM(newRenderElement);
    currentDOM.parentNode.replaceChild(newDom, currentDOM);
    currentElement = newRenderElement;

  //3. 节点类型一致, 进行深度dom diff
  } else {
    // 新老节点都有,并且类型一样。 div span 都要进行dom diff 深度比较 比较他们的属性和他们的子节点
    updateElement(oldRenderElement, newRenderElement)
    // 不会改变类组件实例上的renderElement指向
    // 1. 如果是函数组件,会通过renderElement找到真实dom,更新其自身的属性和深层diff
    // 2. 如果是类组件,是用新的属性来调用自身updater上的emitUpdate让自身属性更新 并让自身对应的真实dom更新
  }

  console.log('return currentElement~~~~~~',currentElement);
  return currentElement;
}
/**
 * 进行dom diff 深度比较 比较他们的属性和他们的子节点
 *
 * 进入到此函数说明它们的 .type是一样的
 * */
function updateElement(oldElement, newElement){
  const currentDOM = newElement.dom = oldElement.dom;

  // 是文本节点 如果老的文本内容 和 新的文本内容不一样就更新
  if(oldElement.$$typeof === TEXT && newElement.$$typeof === TEXT){
    if(oldElement.content !== newElement.content) {
      currentDOM.textContent = newElement.content;
      oldElement.content = newElement.content;
    }

    // 如果是元素类型节点 div、span这些
  }else if (oldElement.$$typeof === ELEMENT) {
    updateDOMProperties(currentDOM, oldElement.props, newElement.props);
    updateChildrenElements(currentDOM, oldElement.props.children, newElement.props.children);

    // updateElement方法没有返回值 不会改变renderElement的指向 每次都会复用同一个老的
    // 我们只需要改变原本renderElement上的props即可(方便下一次比较,props里包含children虚拟dom)
    oldElement.props = newElement.props; /** ←很关键 赋完值之后, 老的虚拟DOM就没有了, 使用新虚拟DOM了*/

    // 参看test-case/03/01updateFunctionComponent.tsx
    // return ( <FunctionCounter number={this.state.number} handleClick={this.handleClick}/> )
  } else if (oldElement.$$typeof === FUNCTION_COMPONENT) {
    updateFunctionComponent(oldElement, newElement);

    // 参看test-case/03/02updateClassComponent.tsx
  } else if (oldElement.$$typeof === CLASS_COMPONENT) {
    updateClassComponent(oldElement, newElement);
  }
}
function updateChildrenElements(dom, oldChildrenElements, newChildrenElements){
  updateDepth++;

  /** 这里为什么要打平 参考05/02getSnapshotBeforeUpdate 和 createRef 这个示例的render返回值*/
  diff(dom, flatten(oldChildrenElements), flatten(newChildrenElements), diffQueue); /** todo 1← 这里使用flatten会导致_mountIndex丢失问题 */
  // diff(dom, oldChildrenElements, newChildrenElements, diffQueue); //这里只会更新属性 和 收集子节点变更补丁(信息)
  updateDepth--;

  if (updateDepth === 0) { //说明回到最上面一层了 比较完了 参看(doc/00先序深度遍历)
    console.log('最终要打的补丁 diffQueue:',diffQueue)
    console.log('最终要打的补丁 diffQueue:',JSON.stringify(diffQueue))
    patch(diffQueue); //新增、删除、或移动
    diffQueue.length = 0;
  }

  /**
   * 1. 先更新父节点还是先更新子节点?(属性和文本节点)
   * 会先更新父节点
   * 2. 先移动父节点还是先移动子节点?(dom)
   * 会先移动子节点 (因为我们是先序深度遍历里打的diff补丁,会先放子节点的补丁,再放父节点的补丁)
   *
   * 但变更子节点都发生在更新属性后
   * */
}
/**
 * 为了正确的使用补丁包需要按如下顺序操作:
 * 1. 把需要移动和需要删除掉的全部删除
 * 2. 把需要移动的和插入的插进来
 *
 * (查看 test-case/04/如何打补丁.png)
 *
 * 如果不按照上面的顺序来做, 先插入后删除
 * ,那么到了删除时用的原来的索引有可能就是错误的了(比如新插入一个元素到要准备删除的要素之前)
 * */
function patch(diffQueue){
  // diffQueue.reverse(); //←从头开始打 或则倒起来打补丁 结果都是一样的 因为我们不会垮层级操作dom 因为补丁里包含parentNode信息 同一层级的同一个父节点的都是同一个parentNode(先序深度遍历带来的效果)
  //第1步要把该删除的全部删除 MOVE REMOVE
  const deleteMap = {};
  const deleteChildren = [];
  for (let i = 0; i < diffQueue.length; i++) {
    const difference = diffQueue[i];
    const { type, fromIndex/** ←老的挂载索引位置*/, toIndex /** ←新的挂载索引位置*/} = difference;
    if (type === MOVE || type === REMOVE) {
      const oldChildDOM = difference.parentNode.children[fromIndex];//先获取老的DOM节点 缓存起来 等会要插回去(如果type为MOVE的话)
      deleteMap[fromIndex] = oldChildDOM;
      deleteChildren.push(oldChildDOM);
    }
  }
  //把移动的和REMOVE全部删除
  deleteChildren.forEach(childDOM => {
    childDOM.parentNode.removeChild(childDOM);
  });
  for (let i = 0; i < diffQueue.length; i++) {
    const { type, fromIndex, toIndex, parentNode, dom } = diffQueue[i];
    switch (type) {
    case INSERT:
      insertChildAt(parentNode, dom, toIndex);
      break;
    case MOVE:
      insertChildAt(parentNode, deleteMap[fromIndex], toIndex);
      break;
    default:
      break;
    }
  }
}
//type可能为INSERT也可能为MOVE↓
//向index这个索引位置插入
function insertChildAt(parentNode, newChildDOM, index) {
  const oldChild = parentNode.children[index];//先取出这个索引位置的老的DOM节点
  oldChild ? parentNode.insertBefore(newChildDOM, oldChild) : parentNode.appendChild(newChildDOM);
}
function diff(parentNode, oldChildrenElements, newChildrenElements){
  const oldChildrenElementsMap = getChildrenElementsMap(oldChildrenElements);

  /** getNewChildrenElementsMap
   * 如果新的key在老的key里能找到对应的,并且对应的类型也相同
   * 1. 会让老的真实DOM节点的属性更新(即会复用老的dom) ↑↑↑【递归(如果有子节点)】
   * 2. 会把老的虚拟DOM赋给新的newChildrenElementsMap作为其成员,这个map的key即是这个虚拟dom的key
   *
   * 如果新的key在老的key里没有找到对应的,则会将新的虚拟dom加入到newChildrenElementsMap作为其成员,这个map的key即是这个虚拟dom的key
   *
   * ↑上面这个过程是递归的, 在让老的真实DOM节点属性更新时, 会检查它的孩子(本代码 是更新自己的属性(updateDOMProperties) 然后检查自己的孩子(updateChildrenElements))
   *
   * 另外这个返回的 newChildrenElementsMap 在后面匹配没用的老节点时有用(检测是否需要REMOVE时)
   * */
  const newChildrenElementsMap = getNewChildrenElementsMap(oldChildrenElementsMap, newChildrenElements);

  let lastIndex = 0;
  for (let i = 0; i < newChildrenElements.length; ++i) {
    const newChildElement = newChildrenElements[i];
    if(newChildElement){
      const newKey = newChildElement.key || i.toString();
      const oldChildElement = oldChildrenElementsMap[newKey];

      // console.log('oldChildElement._mountIndex!!!!:',oldChildElement._mountIndex);
      // 该key在新老children里的有
      if(newChildElement === oldChildElement){ // 说明他们是同一个对象 是复用老节点
        if (oldChildElement._mountIndex < lastIndex) {
          diffQueue.push({
            parentNode, // 要移动哪个父节点下的元素
            type: MOVE,
            fromIndex: oldChildElement._mountIndex,
            toIndex: i
          });
        }
        lastIndex = Math.max(oldChildElement._mountIndex, lastIndex); /** ←lastIndex主要就是用来确认是否需要移动那些可复用的旧节点*/
      }else { // 如果新老元素不相等 或 老的里找不到该索引 则直接插入
        diffQueue.push({
          parentNode,
          type: INSERT,
          toIndex: i,
          dom: createDOM(newChildElement)
        });
      }
      newChildElement._mountIndex = i; // ←这样就可以了 不用讲新的返回 因为在外面(updateElement里) 我们会将oldElement.props = newElement.props; props里包括children 也就是这里的newChildElement
    }else { // newChildElement == null
      // console.log('oldChildrenElements[i]:', oldChildrenElements[i]);
      const newKey = i.toString(); //此时元素都没有了 就只能取索引作为key了
      if(oldChildrenElementsMap[newKey].componentInstance && oldChildrenElementsMap[newKey].componentInstance.componentWillUnmount){
        oldChildrenElementsMap[newKey].componentInstance.componentWillUnmount()
      }
    }
  }
  for(const oldKey in oldChildrenElementsMap){
    if(!newChildrenElementsMap.hasOwnProperty(oldKey)){
      const oldChildElement = oldChildrenElementsMap[oldKey];
      console.log('oldChildElement._mountIndex!!!!~~~~~~:',oldChildElement._mountIndex);
      diffQueue.push({
        parentNode,
        type: REMOVE,
        fromIndex: oldChildElement._mountIndex
      })
    }
  }
}
function getNewChildrenElementsMap(oldChildrenElementsMap,newChildrenElements){
  const newChildrenElementsMap = {};
  for (let i = 0; i < newChildrenElements.length; ++i) {
    const newChildElement = newChildrenElements[i];
    if(newChildElement){
      const newKey = newChildElement.key || i.toString();
      const oldChildElement = oldChildrenElementsMap[newKey];
      //key一样 类型一样 就可以复用
      if(canDeepCompare(oldChildElement, newChildElement)){
        updateElement(oldChildElement, newChildElement); /** ←开始递归 复用老的DOM节点 用新的属性更新这个DOM节点*/
        newChildrenElements[i] = oldChildElement; /** ←复用老的虚拟节点*/
      }
      newChildrenElementsMap[newKey] = newChildrenElements[i];
    }
  }
  return newChildrenElementsMap;
}
function canDeepCompare(oldChildElement, newChildElement){
  if(!!oldChildElement && !!newChildElement){
    return oldChildElement.type === newChildElement.type; //如果类型一样 就可以复用了 进行深度对比了
  }
  return false;
}
function getChildrenElementsMap(oldChildrenElements){
  const oldChildrenElementsMap = {};
  for (let i = 0; i < oldChildrenElements.length; ++i) {
    const oldKey = oldChildrenElements[i].key || i.toString();
    oldChildrenElementsMap[oldKey] = oldChildrenElements[i];
  }
  return oldChildrenElementsMap;
}

// 走到这里是类组件return 包含另外一个类组件的情景 这里的oldElement和newElement是类组件虚拟dom (而不是renderElement)
// 走到这里一定是类组件虚拟dom, 因为就是在这样的分支里调用的
function updateClassComponent(oldElement, newElement){
  /**类组件更新时(Counter),会调用类组件的render方法,会重新生成一个renderElement虚拟dom(我们的ChildCounter会作为这个renderElement的child虚拟dom),注意,是调用component的render方法生成的虚拟dom,而不是ReactDOM.render,也就是说不会调用createDOM和走里面的createClassComponentDOM,也就不会给这个类组件新生成的renderElement里新生成的ChildCounter类组件虚拟dom身上挂载componentInstance属性
  So我们这里这样取↓就会报错 这个componentInstance是不存在的
*/
  // const componentInstance = oldElement.componentInstance;
  /** 需要这样写↓ 只有最初在调用ReactDOM.render时才会给类组件虚拟dom挂上componentInstance这个属性*/
  const componentInstance = newElement.componentInstance = oldElement.componentInstance;

  const updater = componentInstance.$updater;

  const nextProps = newElement.props;

  if(oldElement.type.contextType){
    console.log('componentInstance.context:',componentInstance.context)
    console.log('oldElement.type.contextType.Provider.value:',oldElement.type.contextType.Provider.value)
    console.log('componentInstance.context===oldElement.type.contextType.Provider.value:',componentInstance.context===oldElement.type.contextType.Provider.value);
    componentInstance.context = oldElement.type.contextType.Provider.value
  }

  if(componentInstance.componentWillReceiveProps){
    componentInstance.componentWillReceiveProps(nextProps);
  }
  if(newElement.type.getDerivedStateFromProps/** ←是静态属性*/){
    const newState = newElement.type.getDerivedStateFromProps(nextProps, componentInstance.state);
    if(newState){
      componentInstance.state = {...componentInstance.state, ...newState};
    }else{
      // 什么都不做
    }
  }
  updater.emitUpdate(nextProps);
}
function updateFunctionComponent(oldElement, newElement){
  const oldRenderElement = oldElement.renderElement;
  const newRenderElement = newElement.type(newElement.props);
  const currentElement = compareTwoElements(oldRenderElement, newRenderElement);
  newElement.renderElement = currentElement; // 方便下一次时进行对比
}
function updateDOMProperties(dom, oldProps, newProps){
  patchProps(dom, oldProps, newProps)
}
export function createDOM(element){
  element = onlyOne(element); // emmm todo 2
  const {$$typeof} = element;
  let dom = null;
  if(!$$typeof){ // element 是一个字符串或则数字 ReactDOM.render(123,document.getElementById('root'))
    dom = document.createTextNode(element);
  }else if($$typeof === TEXT){
    dom = document.createTextNode(element.content);
  }else if($$typeof === ELEMENT){
    dom = createNativeDOM(element);
  }else if($$typeof === FUNCTION_COMPONENT){
    dom = createFunctionComponentDOM(element);
  }else if($$typeof === CLASS_COMPONENT){
    dom = createClassComponentDOM(element);
  }

  // 不管什么类型元素 都让它的dom属性指向他创建出来的真实dom元素 compareTwoElements方法中会用到
  element.dom = dom;

  return dom;
}
/** 创建函数组件对应的真实DOM对象*/
function createFunctionComponentDOM(element){
  const {type, props} = element;
  const renderElement = type(props);
  element.renderElement = renderElement; // 指向上一次渲染的虚拟DOM节点 用于dom diff
  return createDOM(renderElement);
}

/** 创建类组件对应的真实DOM对象*/
function createClassComponentDOM(element){
  const {type, props, ref} = element;
  const componentInstance = new type(props);

  if(type.contextType) {
    componentInstance.context = type.contextType.Provider.value;
  }

  if (ref) {
    ref.current = componentInstance;
  }

  /** ↓注意这个虚拟dom是类组件虚拟dom而不是类组件return返回的renderElement*/
  element.componentInstance = componentInstance;

  if(componentInstance.componentWillMount){
    componentInstance.componentWillMount()
  }

  if(element.type.getDerivedStateFromProps/** ←是静态属性*/){
    const newState = element.type.getDerivedStateFromProps(props, componentInstance.state);
    if(newState){
      componentInstance.state = {...componentInstance.state, ...newState};
    }else{
      // 什么都不做
    }
  }

  const renderElement = componentInstance.render();
  componentInstance.renderElement = renderElement; // 指向上一次渲染的虚拟DOM节点 用于dom diff 参看component.tsx中的forceUpdate方法
  const newDom =  createDOM(renderElement);

  //TODO 4 emmm 这么写的话 那么子组件挂载时 还有条件渲染重新挂载时 都不会执行didmount
  //  所以应该在componentInstance从属的虚拟dom上(即类虚拟dom上) 加一个发布订阅
  if(componentInstance && componentInstance.componentDidMount){
    Promise.resolve().then(() => componentInstance.componentDidMount());
  }

  return newDom;
}
function createNativeDOM(element){
  const {type, props, ref} = element;
  const dom = document.createElement(type); // 真实的BUTTON DOM对象
  // 1. 创建此虚拟DOM节点的子节点
  createDOMChildren(dom, element.props.children);
  setProps(dom, props);
  if (ref) {
    ref.current = dom;
  }
  // 2. 给此DOM元素添加属性
  return dom;
}
function createDOMChildren(parentNode, children){
  children && /*children.flat(Infinity)*//**←这个是深克隆的*/flatten(children).forEach((child, index) => {

    //child是虚拟dom,我们会在虚拟DOM上加一个属性_mountIndex,指向此DOM节点在父节点中的索引
    //在后面我们做dom-diff的时候有用
    child._mountIndex = index; /** todo 3 ←只在这里设置应该是有问题的 因为我们的类组件每次渲染都会生成新的renderElement,那么当类组件是条件渲染时,类组件的renderElement将无法复用,意思是从true->false->true,此时新生成的renderElement上将没有_mountIndex，因为只有我们第一次渲染时,即调用ReactDOM.render时才会设置_mountIndex,*/

    const childDOM = createDOM(child); // 创建此虚拟DOM节点的真实DOM元素
    parentNode.appendChild(childDOM);
  });
}



export function ReactElement($$typeof, type, key, ref, props) {
  const element = {
    $$typeof, type, key, ref, props
  };
  return element;
}
